Skip to content

Errors (versions)#3187

Draft
matt-aitken wants to merge 10 commits intomainfrom
errors-add-version-filter
Draft

Errors (versions)#3187
matt-aitken wants to merge 10 commits intomainfrom
errors-add-version-filter

Conversation

@matt-aitken
Copy link
Member

  • Added versions filtering on the Errors list and page
  • Added errors stacked bars to the graph on the individual error page

@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

⚠️ No Changeset found

Latest commit: 91ab04b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@matt-aitken matt-aitken marked this pull request as draft March 5, 2026 23:24
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Walkthrough

Adds version-aware error grouping, filtering, and alerting across UI, backend, ClickHouse, and database. Frontend: new LogsVersionFilter, exported VersionsDropdown, status filters, chart legend class prop, alert configuration UI, and email/slack/webhook delivery components. Backend: presenters and services now accept/return versions and ErrorGroupState; new ErrorGroupActions, DeliverErrorGroupAlertService, ErrorAlertEvaluator, alerts worker tasks, and presenters for alert channels. ClickHouse: per-version occurrence and "since" query builders added. Database: Prisma/schema and SQL migrations introduce ErrorGroupState, enum updates, and alert configuration storage. Email template for error-group alerts added.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete. It lacks required sections from the template including issue reference (Closes #), testing details, and changelog. Only a brief summary is provided. Fill in all required template sections: add issue number, describe testing steps, provide a detailed changelog, and include any relevant screenshots.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Errors (versions)' is partially related to the changeset. It refers to a real aspect of the changes (versions filtering for errors), but does not clearly summarize the main point or is overly broad. Consider using a more descriptive title such as 'Add versions filtering and stacked bars to error logs and details' to better convey the scope of changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch errors-add-version-filter
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Errors list sparklines don't reflect the version filter

When a user filters errors by version on the list page (errors._index/route.tsx), the occurrencesPromise at line 120-134 chains from the filtered listPromise but calls presenter.getOccurrences() without passing the versions filter. This means the sparkline activity graphs shown in each error row will display occurrences across ALL versions, even when the list itself is filtered to specific versions. This may be intentional (showing full activity context) but could confuse users who expect the sparklines to match the filtered view.

(Refers to lines 120-134)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +213 to +219
const data = buckets.map((epoch) => {
const point: Record<string, number | Date> = { date: new Date(epoch * 1000) };
for (const version of sortedVersions) {
point[version] = byBucketVersion.get(`${epoch}:${version}`) ?? 0;
}
return point;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Version string used as Record key can collide with reserved date key, breaking the chart

In getOccurrences, version strings are used as dynamic keys in the same Record<string, number | Date> that already has a date key (apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts:214-217). If a task_version value is literally "date" (or empty-string mapped to "unknown" which is safe, but "date" is not guarded), the version count (a number) overwrites the date field (a Date). Downstream in ActivityChart (apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:428), d.date instanceof Date would be false and new Date(smallNumber).getTime() would produce a nonsensical timestamp (epoch 1970), completely breaking the chart's x-axis. The same collision can occur with "__timestamp" added at route.tsx:427.

Prompt for agents
In apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts, the getOccurrences method (lines 213-219) uses version strings as keys in a Record that also has a "date" key. To prevent collisions, either:

1. Prefix version keys with a safe namespace (e.g., "v:" prefix) and update the chart consumer to strip the prefix when displaying labels, OR
2. Restructure the data to use a separate nested object for version counts, e.g.:
   { date: Date, versions: Record<string, number> }
   and update the ChartRoot consumer in the fingerprint route (route.tsx lines 475-496) to match.

Also in apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx line 427, the __timestamp key added via spread could similarly collide if a version were named "__timestamp". Guard against both reserved keys ("date" and "__timestamp").
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@matt-aitken matt-aitken force-pushed the errors-add-version-filter branch from cd9b5ea to e609e8b Compare March 16, 2026 23:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx (1)

560-574: ⚠️ Potential issue | 🔴 Critical

Missing ERROR_GROUP case in alertTypeTitle will cause runtime error.

The ProjectAlertType enum now includes ERROR_GROUP (added in this PR's schema changes), but alertTypeTitle doesn't handle it. When an alert channel with ERROR_GROUP type is displayed in the table (line 237), this will throw an error.

🐛 Proposed fix
 export function alertTypeTitle(alertType: ProjectAlertType): string {
   switch (alertType) {
     case "TASK_RUN":
       return "Task run failure";
     case "TASK_RUN_ATTEMPT":
       return "Task attempt failure";
     case "DEPLOYMENT_FAILURE":
       return "Deployment failure";
     case "DEPLOYMENT_SUCCESS":
       return "Deployment success";
+    case "ERROR_GROUP":
+      return "Error group";
     default: {
       throw new Error(`Unknown alertType: ${alertType}`);
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
around lines 560 - 574, The switch in alertTypeTitle(ProjectAlertType) is
missing a handler for the new "ERROR_GROUP" enum value and will throw for that
case; update the alertTypeTitle function to add a case for "ERROR_GROUP" (e.g.,
case "ERROR_GROUP": return "Error group";) so all ProjectAlertType variants are
handled and the default throw is avoided when ERROR_GROUP alerts are rendered
(used elsewhere where alerts are displayed).
apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts (1)

64-85: ⚠️ Potential issue | 🔴 Critical

getSummary() declares ErrorGroupSummary as return type but returns an incomplete object without state.

The getSummary() method at line 249 declares a return type of Promise<ErrorGroupSummary | undefined>, where ErrorGroupSummary.state is required. However, the method returns an object (lines 276–285) that omits the state field entirely. While the call() method does assign state after retrieval (line 149), this violates the type contract of getSummary(). Either make state optional in the type definition, include state in getSummary()'s return object, or update getSummary()'s return type to reflect the incomplete object it actually returns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 64
- 85, getSummary() claims to return ErrorGroupSummary but omits the required
state field; update getSummary() to include a state property in its returned
object (either populated from the same data source used by call() or a sensible
default with fields like status, resolvedAt, resolvedInVersion, resolvedBy,
ignoredUntil, ignoredReason, ignoredByUserId, ignoredUntilOccurrenceRate,
ignoredUntilTotalOccurrences) so the returned object matches the
ErrorGroupSummary type, and remove any subsequent reassignment in call() that
assumes state is missing.
🧹 Nitpick comments (4)
internal-packages/clickhouse/src/index.ts (1)

248-263: Add @crumbs tracing annotations for the new error-query paths.

Please add crumb comments around the newly added errors query builder exposure to align with repository tracing standards.

As per coding guidelines, **/*.{ts,tsx,js}: “Add crumbs as you write code using // @crumbs comments or // #region @crumbs blocks for agentcrumbs debug tracing”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal-packages/clickhouse/src/index.ts` around lines 248 - 263, The new
errors getter (get errors) that exposes multiple query-builder helpers
(getGroups, getInstances, getHourlyOccurrences, affectedVersionsQueryBuilder,
listQueryBuilder, occurrencesListQueryBuilder, createOccurrencesQueryBuilder,
createOccurrencesByVersionQueryBuilder, occurrenceCountSinceQueryBuilder,
activeErrorsSinceQueryBuilder, occurrenceCountsSinceQueryBuilder) needs
repository-standard tracing crumbs; wrap the errors block with // `@crumbs`
comments (or a // `#region` `@crumbs` ... // `#endregion` `@crumbs` block) immediately
before and after the getter so agentcrumbs can trace calls to these functions,
ensuring the comments surround the get errors { ... } block and reference the
same function names for clarity.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx (1)

630-632: Consider a more reliable approach for closing the dialog after submission.

Using setTimeout(onClose, 100) is fragile—if the submission takes longer than 100ms to start, the dialog may close before the request begins. Consider using the fetcher's state to close the dialog when submission completes:

♻️ Suggested alternative
// In CustomIgnoreForm, use useEffect to close on success:
import { useEffect } from "react";

// ...

useEffect(() => {
  if (fetcher.state === "idle" && fetcher.data?.ok) {
    onClose();
  }
}, [fetcher.state, fetcher.data, onClose]);

// Remove setTimeout from onSubmit

Or simply close immediately on submit and let the optimistic UI handle it if that's the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 630 - 632, The current onSubmit handler uses setTimeout(onClose,
100) which is fragile; remove the setTimeout call in the onSubmit prop of
CustomIgnoreForm and instead close the dialog when the fetcher completes by
adding a useEffect in the CustomIgnoreForm component that watches fetcher.state
and fetcher.data (e.g., useEffect(() => { if (fetcher.state === "idle" &&
fetcher.data?.ok) onClose(); }, [fetcher.state, fetcher.data, onClose]));
alternatively, if you intend optimistic UI, call onClose() immediately in
onSubmit and omit the timeout.
apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts (1)

30-46: Prefer a type alias for the payload shape.

This new model should use type instead of interface to stay aligned with the repo's TypeScript rules.

As per coding guidelines, **/*.{ts,tsx}: Use types over interfaces for TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts` around
lines 30 - 46, Replace the interface declaration ErrorAlertPayload with a type
alias using the same shape (e.g., type ErrorAlertPayload = { ... }); keep the
property names and nested error object unchanged (including channelId,
projectId, classification, and the error fields like fingerprint, environmentId,
environmentName, taskIdentifier, errorType, errorMessage, sampleStackTrace,
firstSeen, lastSeen, occurrenceCount) so all references to ErrorAlertPayload and
ErrorAlertClassification continue to work; update any imports/exports if
necessary to reflect the type alias.
apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts (1)

16-26: Prefer type aliases for these internal models.

AlertableError and ResolvedEnvironment should use type aliases here to match the repo's TypeScript convention.

As per coding guidelines, **/*.{ts,tsx}: Use types over interfaces for TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts` around
lines 16 - 26, Replace the two internal interfaces with type aliases to match
repo convention: convert interface AlertableError and interface
ResolvedEnvironment into type AlertableError = { ... } and type
ResolvedEnvironment = { ... }, keeping the exact same property names and types
(classification, error, environmentName) and (id, type, displayName)
respectively; update any local references if needed but do not change the shape
or visibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 130-145: getState is being called without the task identifier so
it can return the wrong ErrorGroupState when the same fingerprint exists under
multiple tasks; stop fetching summary and state in parallel and instead first
await this.getSummary(...) to read the taskIdentifier, then call
this.getState(environmentId, taskIdentifier, fingerprint) (or otherwise pass the
task identifier into getState) when you run the remaining Promise.all for
getAffectedVersions, getRunList and getState so state lookup is disambiguated by
taskIdentifier.

In `@apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts`:
- Around line 104-112: The helper `#scheduleErrorAlertEvaluation` currently
enqueues a job using the same id pattern "evaluateErrorAlerts:${projectId}"
that's used by the evaluator's delayed self-chain, causing deduplication/no-op
if a future run is already queued; change the id for this immediate kick to be
unique (for example append a timestamp or a "now" suffix like
"evaluateErrorAlerts:${projectId}:now:${Date.now()}") so alertsWorker.enqueue
always creates a distinct job when triggered immediately after channel
creation/edit; keep the job name "v3.evaluateErrorAlerts" and payload identical,
only alter the id generation logic in `#scheduleErrorAlertEvaluation`.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts`:
- Around line 239-243: The code logs and throws the raw webhook URL
(webhookProperties.data.url) from the channel object (type
ProjectAlertChannelType), which can leak embedded credentials; update
deliverErrorGroupAlert.server.ts to sanitize/redact webhookProperties.data.url
before any logging or including it in thrown errors (e.g., replace
host/path/query credentials with "[REDACTED]" or strip auth/query params) and
ensure any thrown Error messages or processLogger entries use the redacted
value; apply the same change for the other occurrences around the webhook
handling block (also referenced at the second occurrence near lines 310-316) so
no raw webhook URL is emitted.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts`:
- Around line 178-197: The total-occurrence unignore check uses
context.occurrencesSince (window-only) so a threshold like
ignoredUntilTotalOccurrences won’t fire across multiple evaluation windows;
change the logic to compute the cumulative occurrences since the alert was
ignored and compare that to state.ignoredUntilTotalOccurrences. Specifically,
replace the context.occurrencesSince check with a call that gets counts since
state.ignoredAt (or otherwise compute occurrences since state.ignoredAt using
getOccurrenceCountsSince/your occurrence-count helper) and use that cumulative
count in the if that references state.ignoredUntilTotalOccurrences and
state.ignoredAt.
- Around line 102-125: The loop enqueues alerts to every envChannels entry on
every evaluator run, ignoring per-channel intervals; inside the for (const
channel of envChannels) loop (before calling alertsWorker.enqueue) add a guard
that respects the channel's configured interval by comparing it to the
evaluator's current run interval (e.g., currentRunIntervalMinutes or
evaluationIntervalMinutes): only enqueue when the current run aligns with the
channel interval (for example channel.intervalMinutes ===
currentRunIntervalMinutes or channel.intervalMinutes % currentRunIntervalMinutes
=== 0), otherwise continue; update any related loops mentioned (the other
occurrences around the same pattern at the referenced ranges) to use the same
interval-check guard.
- Around line 30-35: The constructor for ErrorAlertEvaluator has its _prisma and
_replica defaults reversed causing writes (e.g., updateErrorGroupStates) to go
to the read-only client; swap the defaults so _prisma defaults to prisma and
_replica defaults to $replica. Update the constructor parameter
order/assignments for ErrorAlertEvaluator accordingly so any write operations
use the primary Prisma client (_prisma = prisma) and reads use the replica
(_replica = $replica).
- Around line 372-387: Remove the stray comma-expression "await this, ..." so
the Prisma update is actually awaited: locate the call to
this._prisma.errorGroupState.update (the block that sets status: "UNRESOLVED"
and clears ignored/resolved fields) and change the statement to await the update
promise directly (i.e., await this._prisma.errorGroupState.update(...)) so the
database write completes before continuing (e.g., before calling selfChain()).

In `@internal-packages/emails/emails/alert-error-group.tsx`:
- Line 58: Change the default export of the component function Email to a named
export (use "export function Email(...)" instead of "export default function
Email(...)") and then update the corresponding import in the emails index module
to use a named import (e.g. import { Email } from '...') or rename on import if
needed; also update any usages that relied on the default import to reference
the named export.

---

Outside diff comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 64-85: getSummary() claims to return ErrorGroupSummary but omits
the required state field; update getSummary() to include a state property in its
returned object (either populated from the same data source used by call() or a
sensible default with fields like status, resolvedAt, resolvedInVersion,
resolvedBy, ignoredUntil, ignoredReason, ignoredByUserId,
ignoredUntilOccurrenceRate, ignoredUntilTotalOccurrences) so the returned object
matches the ErrorGroupSummary type, and remove any subsequent reassignment in
call() that assumes state is missing.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx:
- Around line 560-574: The switch in alertTypeTitle(ProjectAlertType) is missing
a handler for the new "ERROR_GROUP" enum value and will throw for that case;
update the alertTypeTitle function to add a case for "ERROR_GROUP" (e.g., case
"ERROR_GROUP": return "Error group";) so all ProjectAlertType variants are
handled and the default throw is avoided when ERROR_GROUP alerts are rendered
(used elsewhere where alerts are displayed).

---

Nitpick comments:
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 630-632: The current onSubmit handler uses setTimeout(onClose,
100) which is fragile; remove the setTimeout call in the onSubmit prop of
CustomIgnoreForm and instead close the dialog when the fetcher completes by
adding a useEffect in the CustomIgnoreForm component that watches fetcher.state
and fetcher.data (e.g., useEffect(() => { if (fetcher.state === "idle" &&
fetcher.data?.ok) onClose(); }, [fetcher.state, fetcher.data, onClose]));
alternatively, if you intend optimistic UI, call onClose() immediately in
onSubmit and omit the timeout.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts`:
- Around line 30-46: Replace the interface declaration ErrorAlertPayload with a
type alias using the same shape (e.g., type ErrorAlertPayload = { ... }); keep
the property names and nested error object unchanged (including channelId,
projectId, classification, and the error fields like fingerprint, environmentId,
environmentName, taskIdentifier, errorType, errorMessage, sampleStackTrace,
firstSeen, lastSeen, occurrenceCount) so all references to ErrorAlertPayload and
ErrorAlertClassification continue to work; update any imports/exports if
necessary to reflect the type alias.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts`:
- Around line 16-26: Replace the two internal interfaces with type aliases to
match repo convention: convert interface AlertableError and interface
ResolvedEnvironment into type AlertableError = { ... } and type
ResolvedEnvironment = { ... }, keeping the exact same property names and types
(classification, error, environmentName) and (id, type, displayName)
respectively; update any local references if needed but do not change the shape
or visibility.

In `@internal-packages/clickhouse/src/index.ts`:
- Around line 248-263: The new errors getter (get errors) that exposes multiple
query-builder helpers (getGroups, getInstances, getHourlyOccurrences,
affectedVersionsQueryBuilder, listQueryBuilder, occurrencesListQueryBuilder,
createOccurrencesQueryBuilder, createOccurrencesByVersionQueryBuilder,
occurrenceCountSinceQueryBuilder, activeErrorsSinceQueryBuilder,
occurrenceCountsSinceQueryBuilder) needs repository-standard tracing crumbs;
wrap the errors block with // `@crumbs` comments (or a // `#region` `@crumbs` ... //
`#endregion` `@crumbs` block) immediately before and after the getter so agentcrumbs
can trace calls to these functions, ensuring the comments surround the get
errors { ... } block and reference the same function names for clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e7e10a3c-d2f2-429c-85bb-c4d210930768

📥 Commits

Reviewing files that changed from the base of the PR and between cd9b5ea and e609e8b.

📒 Files selected for processing (21)
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/models/projectAlert.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/v3/alertsWorker.server.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts
  • apps/webapp/app/v3/services/errorGroupActions.server.ts
  • internal-packages/clickhouse/src/errors.ts
  • internal-packages/clickhouse/src/index.ts
  • internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql
  • internal-packages/database/prisma/migrations/20260308181657_add_error_alert_config_to_project_alert_channel/migration.sql
  • internal-packages/database/prisma/schema.prisma
  • internal-packages/emails/emails/alert-error-group.tsx
  • internal-packages/emails/src/index.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • internal-packages/clickhouse/src/errors.ts

Comment on lines +130 to 145
const [summary, affectedVersions, runList, stateRow] = await Promise.all([
this.getSummary(organizationId, projectId, environmentId, fingerprint),
this.getAffectedVersions(organizationId, projectId, environmentId, fingerprint),
this.getRunList(organizationId, environmentId, {
userId,
projectId,
fingerprint,
versions,
pageSize: runsPageSize,
from: time.from.getTime(),
to: time.to.getTime(),
cursor,
direction,
}),
this.getState(environmentId, fingerprint),
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

State lookup needs the task identifier.

ErrorGroupState is keyed by environment + taskIdentifier + fingerprint elsewhere in this PR, but this query only filters by environment and fingerprint. If the same fingerprint exists under multiple tasks, the page can show the wrong resolved/ignored state; fetching summary and stateRow in the same Promise.all also prevents you from disambiguating it.

Also applies to: 318-340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 130
- 145, getState is being called without the task identifier so it can return the
wrong ErrorGroupState when the same fingerprint exists under multiple tasks;
stop fetching summary and state in parallel and instead first await
this.getSummary(...) to read the taskIdentifier, then call
this.getState(environmentId, taskIdentifier, fingerprint) (or otherwise pass the
task identifier into getState) when you run the remaining Promise.all for
getAffectedVersions, getRunList and getState so state lookup is disambiguated by
taskIdentifier.

Comment on lines +104 to +112
async #scheduleErrorAlertEvaluation(projectId: string): Promise<void> {
await alertsWorker.enqueue({
id: `evaluateErrorAlerts:${projectId}`,
job: "v3.evaluateErrorAlerts",
payload: {
projectId,
scheduledAt: Date.now(),
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't reuse the periodic job id for the immediate kick.

This helper uses the same evaluateErrorAlerts:${projectId} id that the evaluator's delayed self-chain uses. If a future run is already queued, this enqueue can become a no-op, so newly enabled or edited ERROR_GROUP channels may not be evaluated until the old availableAt.

Suggested fix
   async `#scheduleErrorAlertEvaluation`(projectId: string): Promise<void> {
     await alertsWorker.enqueue({
-      id: `evaluateErrorAlerts:${projectId}`,
+      id: `evaluateErrorAlerts:manual:${projectId}:${Date.now()}`,
       job: "v3.evaluateErrorAlerts",
       payload: {
         projectId,
         scheduledAt: Date.now(),
       },
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async #scheduleErrorAlertEvaluation(projectId: string): Promise<void> {
await alertsWorker.enqueue({
id: `evaluateErrorAlerts:${projectId}`,
job: "v3.evaluateErrorAlerts",
payload: {
projectId,
scheduledAt: Date.now(),
},
});
async `#scheduleErrorAlertEvaluation`(projectId: string): Promise<void> {
await alertsWorker.enqueue({
id: `evaluateErrorAlerts:manual:${projectId}:${Date.now()}`,
job: "v3.evaluateErrorAlerts",
payload: {
projectId,
scheduledAt: Date.now(),
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts` around lines
104 - 112, The helper `#scheduleErrorAlertEvaluation` currently enqueues a job
using the same id pattern "evaluateErrorAlerts:${projectId}" that's used by the
evaluator's delayed self-chain, causing deduplication/no-op if a future run is
already queued; change the id for this immediate kick to be unique (for example
append a timestamp or a "now" suffix like
"evaluateErrorAlerts:${projectId}:now:${Date.now()}") so alertsWorker.enqueue
always creates a distinct job when triggered immediately after channel
creation/edit; keep the job name "v3.evaluateErrorAlerts" and payload identical,
only alter the id generation logic in `#scheduleErrorAlertEvaluation`.

Comment on lines +239 to +243
channel: {
type: ProjectAlertChannelType;
properties: unknown;
project: { id: string; externalRef: string; slug: string; name: string; organizationId: string; organization: { slug: string; title: string } };
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact webhook URLs from logs and thrown errors.

Webhook URLs commonly embed credentials in the path or query string. Logging webhookProperties.data.url and including it in the thrown error will leak those secrets into worker logs and retry metadata.

Suggested fix
   async `#sendWebhook`(
     channel: {
+      id: string;
       type: ProjectAlertChannelType;
       properties: unknown;
       project: { id: string; externalRef: string; slug: string; name: string; organizationId: string; organization: { slug: string; title: string } };
     },
@@
     if (!response.ok) {
       logger.info("[DeliverErrorGroupAlert] Failed to send webhook", {
         status: response.status,
         statusText: response.statusText,
-        url: webhookProperties.data.url,
+        channelId: channel.id,
       });
-      throw new Error(`Failed to send error group alert webhook to ${webhookProperties.data.url}`);
+      throw new Error("Failed to send error group alert webhook");
     }

Also applies to: 310-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts` around
lines 239 - 243, The code logs and throws the raw webhook URL
(webhookProperties.data.url) from the channel object (type
ProjectAlertChannelType), which can leak embedded credentials; update
deliverErrorGroupAlert.server.ts to sanitize/redact webhookProperties.data.url
before any logging or including it in thrown errors (e.g., replace
host/path/query credentials with "[REDACTED]" or strip auth/query params) and
ensure any thrown Error messages or processLogger entries use the redacted
value; apply the same change for the other occurrences around the webhook
handling block (also referenced at the second occurrence near lines 310-316) so
no raw webhook URL is emitted.

Comment on lines +102 to +125
for (const alertable of alertableErrors) {
const envChannels = channelsByEnvId.get(alertable.error.environment_id) ?? [];
for (const channel of envChannels) {
await alertsWorker.enqueue({
job: "v3.deliverErrorGroupAlert",
payload: {
channelId: channel.id,
projectId,
classification: alertable.classification,
error: {
fingerprint: alertable.error.error_fingerprint,
environmentId: alertable.error.environment_id,
environmentName: alertable.environmentName,
taskIdentifier: alertable.error.task_identifier,
errorType: alertable.error.error_type,
errorMessage: alertable.error.error_message,
sampleStackTrace: alertable.error.sample_stack_trace,
firstSeen: alertable.error.first_seen,
lastSeen: alertable.error.last_seen,
occurrenceCount: alertable.error.occurrence_count,
},
},
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Longer channel intervals are effectively ignored.

The evaluator schedules the whole project at the shortest configured interval, but the delivery loop still enqueues alerts for every matching channel on every run. That means a channel configured for 60 minutes will still receive alerts every 5 minutes if any sibling channel uses a 5-minute interval.

Also applies to: 212-221, 391-404

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts` around
lines 102 - 125, The loop enqueues alerts to every envChannels entry on every
evaluator run, ignoring per-channel intervals; inside the for (const channel of
envChannels) loop (before calling alertsWorker.enqueue) add a guard that
respects the channel's configured interval by comparing it to the evaluator's
current run interval (e.g., currentRunIntervalMinutes or
evaluationIntervalMinutes): only enqueue when the current run aligns with the
channel interval (for example channel.intervalMinutes ===
currentRunIntervalMinutes or channel.intervalMinutes % currentRunIntervalMinutes
=== 0), otherwise continue; update any related loops mentioned (the other
occurrences around the same pattern at the referenced ranges) to use the same
interval-check guard.

project: "my-project",
};

export default function Email(props: AlertErrorGroupEmailProps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify default export removal in the new email file
rg -nP --type=tsx '^\s*export\s+default\b' internal-packages/emails/emails/alert-error-group.tsx

# Verify named import usage in EmailClient index
rg -nP --type=tsx 'alert-error-group' internal-packages/emails/src/index.tsx -A3 -B3

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Search for default export in alert-error-group file
rg -n 'export\s+default' internal-packages/emails/emails/alert-error-group.tsx

# Check the import statement in index.tsx
rg -n 'alert-error-group' internal-packages/emails/src/index.tsx -A5 -B2

# Get file line counts to know safe read ranges
wc -l internal-packages/emails/emails/alert-error-group.tsx internal-packages/emails/src/index.tsx

Repository: triggerdotdev/trigger.dev

Length of output: 906


🏁 Script executed:

# Read the alert-error-group file around the export line
sed -n '55,65p' internal-packages/emails/emails/alert-error-group.tsx

# Check if this export is imported/used anywhere else
rg -n 'AlertErrorGroupEmail' internal-packages/emails/ --type=ts --type=tsx

# Check the export in index.tsx around the import
sed -n '1,20p' internal-packages/emails/src/index.tsx

Repository: triggerdotdev/trigger.dev

Length of output: 1261


🏁 Script executed:

# Check all usages of AlertErrorGroupEmail across the codebase
rg -n 'AlertErrorGroupEmail' --max-count=20

# Also check if there are any other references to alert-error-group file
rg -n 'alert-error-group' internal-packages/ --max-count=20

Repository: triggerdotdev/trigger.dev

Length of output: 1203


Replace the default export with a named function export.

Line 58 introduces a default export, which violates the repository export rule. Switch to a named export and update the import in internal-packages/emails/src/index.tsx accordingly.

Suggested patch
- export default function Email(props: AlertErrorGroupEmailProps) {
+ export function AlertErrorGroupEmail(props: AlertErrorGroupEmailProps) {
   const {
     classification,
     taskIdentifier,
     environment,
     error,
     occurrenceCount,
     errorLink,
-import AlertErrorGroupEmail, {
-  AlertErrorGroupEmailSchema,
-} from "../emails/alert-error-group";
+import {
+  AlertErrorGroupEmail,
+  AlertErrorGroupEmailSchema,
+} from "../emails/alert-error-group";

Per coding guidelines: **/*.{ts,tsx,js,jsx}: Use function declarations instead of default exports

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function Email(props: AlertErrorGroupEmailProps) {
export function AlertErrorGroupEmail(props: AlertErrorGroupEmailProps) {
const {
classification,
taskIdentifier,
environment,
error,
occurrenceCount,
errorLink,
Suggested change
export default function Email(props: AlertErrorGroupEmailProps) {
import {
AlertErrorGroupEmail,
AlertErrorGroupEmailSchema,
} from "../emails/alert-error-group";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal-packages/emails/emails/alert-error-group.tsx` at line 58, Change the
default export of the component function Email to a named export (use "export
function Email(...)" instead of "export default function Email(...)") and then
update the corresponding import in the emails index module to use a named import
(e.g. import { Email } from '...') or rename on import if needed; also update
any usages that relied on the default import to reference the named export.

@matt-aitken matt-aitken force-pushed the errors-add-version-filter branch from 6875ac8 to 497f301 Compare March 20, 2026 15:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx (1)

119-125: ⚠️ Potential issue | 🟠 Major

Apply versions to the activity query as well.

This only filters the list query. The occurrencesPromise below still calls presenter.getOccurrences() without versions, so after selecting a version the counts change but the Activity sparkline still shows all versions. Please thread the same filter into that query too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
around lines 119 - 125, The activity counts query is not receiving the versions
filter — mirror how you pass versions into the list query by including versions
(only when versions.length > 0 ? versions : undefined) in the call to
presenter.getOccurrences for occurrencesPromise; update the occurrencesPromise
invocation (presenter.getOccurrences) to accept the same parameters used for
listPromise (project.organizationId, environment.id, { userId, projectId:
project.id, tasks: tasks.length > 0 ? tasks : undefined, versions:
versions.length > 0 ? versions : undefined, statuses: statuses.length > 0 ?
statuses : undefined }) so the Activity sparkline respects the selected version
filter.
apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts (1)

273-299: ⚠️ Potential issue | 🟠 Major

Filter by status before computing cursors.

Pagination is already finalized before this block, then Lines 295-299 drop rows in memory. With a status filter applied, that can return short or empty pages even when more matching groups exist later, and the cursor can skip unseen matches. Filter before slicing/cursor generation, or over-fetch until you collect pageSize matching groups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts` around lines 273
- 299, The transformedErrorGroups are filtered by statuses after
pagination/cursor logic which can produce short or skipped pages; change the
flow so you apply the status filter immediately after building
transformedErrorGroups (before any slicing/cursor generation) or implement
over-fetching: iterate the original errorGroups in order, map + filter by
statuses (using getGlobalSummary/getErrorGroupStates results) and collect until
you have pageSize matches (fetching more if needed) before creating cursors;
reference transformedErrorGroups, statuses, pageSize, getGlobalSummary and
getErrorGroupStates to locate where to apply the filter or implement the
over-fetch loop.
apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts (1)

76-85: ⚠️ Potential issue | 🔴 Critical

getSummary() returns an object missing the required state field.

The return type ErrorGroupSummary (line 85) requires state: ErrorGroupState, but the object literal returned at lines 276–285 does not include it. This causes a TypeScript compilation error. While call() decorates the summary with state afterwards, getSummary() must satisfy its declared return type contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 76
- 85, getSummary() currently returns an object missing the required state field
declared on the ErrorGroupSummary type; update getSummary() to include a proper
state property (of type ErrorGroupState) in the returned summary object (or
compute a default/derived state inside getSummary()) so its return satisfies the
ErrorGroupSummary contract, and ensure call() continues to decorate or override
state if needed; reference getSummary(), ErrorGroupSummary and call() when
making the change.
♻️ Duplicate comments (3)
apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts (1)

104-113: ⚠️ Potential issue | 🟡 Minor

Job deduplication may prevent immediate evaluation.

The stable evaluateErrorAlerts:${projectId} ID is shared with the evaluator's delayed self-chain. If a future evaluation job is already queued (e.g., scheduled to run in a few minutes), this enqueue becomes a no-op, and newly enabled ERROR_GROUP channels won't be evaluated until the existing job runs.

Consider using a unique ID for immediate kicks to ensure prompt evaluation after channel creation/update:

Suggested fix
  async `#scheduleErrorAlertEvaluation`(projectId: string): Promise<void> {
    await alertsWorker.enqueue({
-     id: `evaluateErrorAlerts:${projectId}`,
+     id: `evaluateErrorAlerts:immediate:${projectId}:${Date.now()}`,
      job: "v3.evaluateErrorAlerts",
      payload: {
        projectId,
        scheduledAt: Date.now(),
      },
    });
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts` around lines
104 - 113, The current `#scheduleErrorAlertEvaluation` uses a stable job id
`evaluateErrorAlerts:${projectId}` which can be deduplicated by the worker queue
and become a no-op if a future evaluator job already exists; change the enqueue
id for this immediate kick to be unique (e.g., append a timestamp or UUID such
as `evaluateErrorAlerts:${projectId}:kick:${Date.now()}`) so the job always gets
scheduled immediately, or alternatively pass any supported "dedupe: false"
option if the queue library exposes it; update the alertsWorker.enqueue call in
`#scheduleErrorAlertEvaluation` accordingly.
apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts (1)

313-320: ⚠️ Potential issue | 🟠 Major

Avoid logging and including webhook URLs in error messages.

Webhook URLs may contain embedded credentials in the path or query string. Logging the URL and including it in the thrown error exposes these secrets in worker logs and retry metadata.

Suggested fix
    if (!response.ok) {
      logger.info("[DeliverErrorGroupAlert] Failed to send webhook", {
        status: response.status,
        statusText: response.statusText,
-       url: webhookProperties.data.url,
+       channelId: payload.channelId,
      });
-     throw new Error(`Failed to send error group alert webhook to ${webhookProperties.data.url}`);
+     throw new Error("Failed to send error group alert webhook");
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts` around
lines 313 - 320, The code logs and throws an error that includes the raw webhook
URL (webhookProperties.data.url) which may expose credentials; update the
failure handling in deliverErrorGroupAlert (the block using logger.info,
response, and webhookProperties) to stop logging or throwing the full URL:
remove webhookProperties.data.url from logger.info payload and from the thrown
Error, instead log only non-sensitive metadata (e.g., response.status,
response.statusText, webhook id or sanitized hostname via new URL(...).host) and
throw an Error with a generic message (e.g., "Failed to send error group alert
webhook" or include only webhookProperties.id), ensuring no unmasked URL or
query/path credentials are emitted.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx (1)

215-245: ⚠️ Potential issue | 🟠 Major

The page is still fingerprint-scoped, but the new actions are task-scoped.

ErrorGroupActions identifies state by taskIdentifier + errorFingerprint, yet this loader still fetches summary, activity, and runs by fingerprint alone. If the same fingerprint exists under multiple tasks, the page can render aggregate data for all of them and then resolve/ignore only whichever taskIdentifier happened to come back in the summary. Thread taskIdentifier through the route and presenter queries before exposing these buttons.

Also applies to: 415-419

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 215 - 245, The loader is fetching summary, activity and runs by
fingerprint only while ErrorGroupActions uses task-scoped identifiers
(taskIdentifier + errorFingerprint); update the route loader to accept and
forward the taskIdentifier and pass it into the presenter queries (e.g., the
call(...) invocation that returns detailPromise, presenter.getOccurrences(...),
and any presenter.getRuns/getSummary methods used later) so the presenter
queries use both taskIdentifier and fingerprint (or a single task-scoped
identifier) instead of fingerprint-alone; ensure the route params and any
timeFilterFromTo usage thread taskIdentifier through to presenter methods before
rendering/exposing ErrorGroupActions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 80-98: The actionSchema currently allows empty taskIdentifier and
non-positive/zero ignore thresholds; update the discriminated union to enforce
taskIdentifier as a non-empty string (e.g., min length 1) and make duration,
occurrenceRate, and totalOccurrences coerce to numbers and require them to be
positive (e.g., .min(1) or .positive()) so empty/zero/negative values are
rejected server-side; apply these validators to the relevant branches in
actionSchema (the "resolve", "ignore", and "unresolve" objects) so
taskIdentifier, duration, occurrenceRate, and totalOccurrences are validated
before persisting.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx:
- Around line 81-150: The action creates/replaces channels via
CreateAlertChannelService.call using CreateAlertChannelOptions but never
persists the alert configuration (errorAlertConfig), so channels end up with
null configs; update the route to include the validated errorAlertConfig in the
options passed to CreateAlertChannelService.call (for email, slack and webhook
flows) and ensure CreateAlertChannelService
(apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts) writes that
errorAlertConfig to the DB on both create and update paths; reference the
CreateAlertChannelOptions type, CreateAlertChannelService.call invocations, and
the errorAlertConfig field when adding the config to the options before calling
the service and before deleting/replacing existing channels.

---

Outside diff comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 76-85: getSummary() currently returns an object missing the
required state field declared on the ErrorGroupSummary type; update getSummary()
to include a proper state property (of type ErrorGroupState) in the returned
summary object (or compute a default/derived state inside getSummary()) so its
return satisfies the ErrorGroupSummary contract, and ensure call() continues to
decorate or override state if needed; reference getSummary(), ErrorGroupSummary
and call() when making the change.

In `@apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts`:
- Around line 273-299: The transformedErrorGroups are filtered by statuses after
pagination/cursor logic which can produce short or skipped pages; change the
flow so you apply the status filter immediately after building
transformedErrorGroups (before any slicing/cursor generation) or implement
over-fetching: iterate the original errorGroups in order, map + filter by
statuses (using getGlobalSummary/getErrorGroupStates results) and collect until
you have pageSize matches (fetching more if needed) before creating cursors;
reference transformedErrorGroups, statuses, pageSize, getGlobalSummary and
getErrorGroupStates to locate where to apply the filter or implement the
over-fetch loop.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx:
- Around line 119-125: The activity counts query is not receiving the versions
filter — mirror how you pass versions into the list query by including versions
(only when versions.length > 0 ? versions : undefined) in the call to
presenter.getOccurrences for occurrencesPromise; update the occurrencesPromise
invocation (presenter.getOccurrences) to accept the same parameters used for
listPromise (project.organizationId, environment.id, { userId, projectId:
project.id, tasks: tasks.length > 0 ? tasks : undefined, versions:
versions.length > 0 ? versions : undefined, statuses: statuses.length > 0 ?
statuses : undefined }) so the Activity sparkline respects the selected version
filter.

---

Duplicate comments:
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 215-245: The loader is fetching summary, activity and runs by
fingerprint only while ErrorGroupActions uses task-scoped identifiers
(taskIdentifier + errorFingerprint); update the route loader to accept and
forward the taskIdentifier and pass it into the presenter queries (e.g., the
call(...) invocation that returns detailPromise, presenter.getOccurrences(...),
and any presenter.getRuns/getSummary methods used later) so the presenter
queries use both taskIdentifier and fingerprint (or a single task-scoped
identifier) instead of fingerprint-alone; ensure the route params and any
timeFilterFromTo usage thread taskIdentifier through to presenter methods before
rendering/exposing ErrorGroupActions.

In `@apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts`:
- Around line 104-113: The current `#scheduleErrorAlertEvaluation` uses a stable
job id `evaluateErrorAlerts:${projectId}` which can be deduplicated by the
worker queue and become a no-op if a future evaluator job already exists; change
the enqueue id for this immediate kick to be unique (e.g., append a timestamp or
UUID such as `evaluateErrorAlerts:${projectId}:kick:${Date.now()}`) so the job
always gets scheduled immediately, or alternatively pass any supported "dedupe:
false" option if the queue library exposes it; update the alertsWorker.enqueue
call in `#scheduleErrorAlertEvaluation` accordingly.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts`:
- Around line 313-320: The code logs and throws an error that includes the raw
webhook URL (webhookProperties.data.url) which may expose credentials; update
the failure handling in deliverErrorGroupAlert (the block using logger.info,
response, and webhookProperties) to stop logging or throwing the full URL:
remove webhookProperties.data.url from logger.info payload and from the thrown
Error, instead log only non-sensitive metadata (e.g., response.status,
response.statusText, webhook id or sanitized hostname via new URL(...).host) and
throw an Error with a generic message (e.g., "Failed to send error group alert
webhook" or include only webhookProperties.id), ensuring no unmasked URL or
query/path credentials are emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a52379f0-9e18-402e-b980-5af54bcd3984

📥 Commits

Reviewing files that changed from the base of the PR and between e609e8b and 91ab04b.

📒 Files selected for processing (27)
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/models/projectAlert.server.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/alertsWorker.server.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts
  • apps/webapp/app/v3/services/errorGroupActions.server.ts
  • internal-packages/clickhouse/src/errors.ts
  • internal-packages/clickhouse/src/index.ts
  • internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql
  • internal-packages/database/prisma/migrations/20260308181657_add_error_alert_config_to_project_alert_channel/migration.sql
  • internal-packages/database/prisma/migrations/20260320115950_add_ignored_at_occurrence_count_to_error_group_state/migration.sql
  • internal-packages/database/prisma/schema.prisma
  • internal-packages/emails/emails/alert-error-group.tsx
  • internal-packages/emails/src/index.tsx
✅ Files skipped from review due to trivial changes (7)
  • internal-packages/database/prisma/migrations/20260320115950_add_ignored_at_occurrence_count_to_error_group_state/migration.sql
  • internal-packages/database/prisma/migrations/20260308181657_add_error_alert_config_to_project_alert_channel/migration.sql
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/v3/services/errorGroupActions.server.ts
  • internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql
  • apps/webapp/app/v3/alertsWorker.server.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/webapp/app/models/projectAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • internal-packages/emails/src/index.tsx
  • internal-packages/clickhouse/src/index.ts
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts
  • internal-packages/emails/emails/alert-error-group.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Use task export syntax: export const myTask = task({ id: 'my-task', run: async (payload) => { ... } })
Use Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work - avoid DEPRECATED zodworker (Graphile-worker wrapper)
Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • internal-packages/clickhouse/src/errors.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • internal-packages/clickhouse/src/errors.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • internal-packages/clickhouse/src/errors.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • internal-packages/clickhouse/src/errors.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/{webapp,supervisor}/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying only server components (apps/webapp/, apps/supervisor/) with no package changes, add a .server-changes/ file instead of a changeset

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Always import from @trigger.dev/sdk for Trigger.dev tasks - never use @trigger.dev/sdk/v3 or deprecated client.defineJob
Import subpaths only from @trigger.dev/core, never import from root
Add crumbs as you write code using // @crumbs comments or // #region @crumbs blocks for agentcrumbs debug tracing

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • internal-packages/clickhouse/src/errors.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/webapp/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server

Files:

  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/webapp/app/v3/services/**/*.server.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Organize services in the webapp following the pattern app/v3/services/*/*.server.ts

Files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
apps/webapp/**/*.server.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Access environment variables via the env export from app/env.server.ts, never use process.env directly

Files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/webapp/app/v3/services/**/*.server.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

When editing services that branch on RunEngineVersion to support both V1 and V2 (e.g., cancelTaskRun.server.ts, batchTriggerV3.server.ts), only modify V2 code paths

Files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
apps/webapp/app/v3/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying V3 code paths in apps/webapp/app/v3/, only modify V2 code - consult apps/webapp/CLAUDE.md for V1-only legacy code to avoid

Files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
apps/webapp/app/routes/**/*.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Use Remix flat-file route convention with dot-separated segments (e.g., api.v1.tasks.$taskId.trigger.ts for /api/v1/tasks/:taskId/trigger)

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
🧠 Learnings (36)
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to apps/webapp/app/v3/**/*.{ts,tsx} : When modifying V3 code paths in apps/webapp/app/v3/, only modify V2 code - consult apps/webapp/CLAUDE.md for V1-only legacy code to avoid

Applied to files:

  • apps/webapp/app/utils/pathBuilder.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/v3/*Worker.server.ts : New background job workers should use `trigger.dev/redis-worker` (e.g., `commonWorker.server.ts`, `alertsWorker.server.ts`, `batchTriggerWorker.server.ts`), not zodworker or graphile-worker

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schedules.task()` for scheduled/cron tasks instead of regular `task()`

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach cron schedules declaratively using the `cron` property or imperatively using `schedules.create()`

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `idempotencyKeys.create()` to create idempotency keys for preventing duplicate task executions

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/**/*.server.ts : Access environment variables via the `env` export from `app/env.server.ts`, never use `process.env` directly

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/**/*.{ts,tsx} : Access all environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env` in the Trigger.dev webapp

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/**/*.test.{ts,tsx} : In test files, do not import `env.server.ts` directly; pass configuration as constructor arguments or options instead for testable code

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
📚 Learning: 2026-03-10T17:56:20.938Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3201
File: apps/webapp/app/v3/services/setSeatsAddOn.server.ts:25-29
Timestamp: 2026-03-10T17:56:20.938Z
Learning: Do not implement local userId-to-organizationId authorization checks inside org-scoped service classes (e.g., SetSeatsAddOnService, SetBranchesAddOnService) in the web app. Rely on route-layer authentication (requireUserId(request)) and org membership enforcement via the _app.orgs.$organizationSlug layout route. Any userId/organizationId that reaches these services from org-scoped routes has already been validated. Apply this pattern across all org-scoped services to avoid redundant auth checks and maintain consistency.

Applied to files:

  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to apps/webapp/**/*.{ts,tsx,jsx,js} : Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/routes/**/*.ts : Use Remix flat-file route convention with dot-separated segments (e.g., `api.v1.tasks.$taskId.trigger.ts` for `/api/v1/tasks/:taskId/trigger`)

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
📚 Learning: 2026-02-11T16:50:14.167Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx:126-131
Timestamp: 2026-02-11T16:50:14.167Z
Learning: In apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx, MetricsDashboard entities are intentionally scoped to the organization level, not the project level. The dashboard lookup should filter by organizationId only (not projectId), allowing dashboards to be accessed across projects within the same organization. The optional projectId field on MetricsDashboard serves other purposes and should not be used as an authorization constraint.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-03-13T13:45:39.411Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/routes/admin.llm-models.missing.$model.tsx:19-21
Timestamp: 2026-03-13T13:45:39.411Z
Learning: In `apps/webapp/app/routes/admin.llm-models.missing.$model.tsx`, the `decodeURIComponent(params.model ?? "")` call is intentionally unguarded. Remix route params are decoded at the routing layer before reaching the loader, so malformed percent-encoding is rejected upstream. The page is also admin-only, so the risk is minimal and no try-catch is warranted.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
📚 Learning: 2026-02-03T18:27:49.039Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:49.039Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (like the Edit button with PencilSquareIcon) intentionally have no text labels - only icons are shown in the TableCellMenu. This is a deliberate UI design pattern for compact icon-only menu items.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-10T16:18:48.654Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 2980
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx:512-515
Timestamp: 2026-02-10T16:18:48.654Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx, environment.queueSizeLimit is a per-queue maximum that is configured at the environment level, not a shared limit across all queues. Each queue can have up to environment.queueSizeLimit items queued independently.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/v3/presenters/**/*.server.{ts,tsx} : Organize presenters in the webapp following the pattern `app/v3/presenters/*/*.server.ts` to move complex loader code into classes

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
📚 Learning: 2026-02-06T19:53:38.843Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts:233-237
Timestamp: 2026-02-06T19:53:38.843Z
Learning: When constructing Vercel dashboard URLs from deployment IDs, always strip the dpl_ prefix from the ID. Implement this by transforming the ID with .replace(/^dpl_/, "") before concatenating into the URL: https://vercel.com/${teamSlug}/${projectName}/${cleanedDeploymentId}. Consider centralizing this logic in a small helper (e.g., getVercelDeploymentId(id) or a URL builder) and add tests to verify both prefixed and non-prefixed inputs.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `useRun`, `useRealtimeRun` and other SWR/realtime hooks from `trigger.dev/react-hooks` for data fetching

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
📚 Learning: 2025-07-12T18:06:04.133Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2264
File: apps/webapp/app/services/runsRepository.server.ts:172-174
Timestamp: 2025-07-12T18:06:04.133Z
Learning: In apps/webapp/app/services/runsRepository.server.ts, the in-memory status filtering after fetching runs from Prisma is intentionally used as a workaround for ClickHouse data delays. This approach is acceptable because the result set is limited to a maximum of 100 runs due to pagination, making the performance impact negligible.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2025-07-12T18:00:06.163Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2264
File: apps/webapp/app/utils/searchParams.ts:16-18
Timestamp: 2025-07-12T18:00:06.163Z
Learning: The `objectToSearchParams` function in `apps/webapp/app/utils/searchParams.ts` is used to generate URL parameters from objects and is separate from code that parses incoming search parameters. Changes to this function only affect places where it's used to create URLs, not places that parse search parameters from external sources.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : Use zod for validation in packages/core and apps/webapp

Applied to files:

  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-01-28T14:15:15.011Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2953
File: apps/webapp/app/components/runs/v3/SharedFilters.tsx:441-452
Timestamp: 2026-01-28T14:15:15.011Z
Learning: In apps/webapp/app/components/runs/v3/SharedFilters.tsx, the maxPeriodDays limit for date ranges should only check the from date (via dateRangeToDays(fromValue)) because it enforces data retention limits—how far back in history queries can reach. The to date is irrelevant for retention-based limits.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to **/*.{ts,tsx} : Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client

Applied to files:

  • internal-packages/database/prisma/schema.prisma
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/v3/services/**/*.server.ts : When editing services that branch on `RunEngineVersion` to support both V1 and V2 (e.g., `cancelTaskRun.server.ts`, `batchTriggerV3.server.ts`), only modify V2 code paths

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2025-06-14T08:07:46.625Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 2175
File: apps/webapp/app/services/environmentMetricsRepository.server.ts:202-207
Timestamp: 2025-06-14T08:07:46.625Z
Learning: In apps/webapp/app/services/environmentMetricsRepository.server.ts, the ClickHouse methods (getTaskActivity, getCurrentRunningStats, getAverageDurations) intentionally do not filter by the `tasks` parameter at the ClickHouse level, even though the tasks parameter is accepted by the public methods. This is done on purpose as there is not much benefit from adding that filtering at the ClickHouse layer.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
📚 Learning: 2026-01-08T15:57:09.323Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/otel-metrics.mdc:0-0
Timestamp: 2026-01-08T15:57:09.323Z
Learning: Applies to **/*.ts : Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-03-04T16:40:11.313Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3172
File: apps/webapp/app/utils/semver.ts:19-33
Timestamp: 2026-03-04T16:40:11.313Z
Learning: In `apps/webapp/app/utils/semver.ts`, the versioning scheme used is date-based (e.g. `20260304.1`, `20260304.2`), not semantic versioning. Pre-release suffix handling (e.g. `-beta`, `-rc`) is not a concern for this file.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Leverage the PostgreSQL database through the `trigger.dev/database` Prisma 6.14.0 client in the webapp for all data access patterns

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts

Comment on lines +80 to +98
const actionSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("resolve"),
taskIdentifier: z.string(),
resolvedInVersion: z.string().optional(),
}),
z.object({
action: z.literal("ignore"),
taskIdentifier: z.string(),
duration: z.coerce.number().optional(),
occurrenceRate: z.coerce.number().optional(),
totalOccurrences: z.coerce.number().optional(),
reason: z.string().optional(),
}),
z.object({
action: z.literal("unresolve"),
taskIdentifier: z.string(),
}),
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant section
fd "route.tsx" apps/webapp/app/routes --exec wc -l {} \; | head -5

Repository: triggerdotdev/trigger.dev

Length of output: 382


🏁 Script executed:

# Try to find and read the file
git ls-files "apps/webapp/app/routes/*errors*" | head -10

Repository: triggerdotdev/trigger.dev

Length of output: 518


🏁 Script executed:

# Search for the error fingerprint route
rg "errorGroup.taskIdentifier|ErrorGroupState" apps/webapp --type ts --type tsx -A 2 -B 2 | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Read the target file
cat -n "apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx" | head -110 | tail -40

Repository: triggerdotdev/trigger.dev

Length of output: 167


🏁 Script executed:

# Search for ErrorGroupState and taskIdentifier usage
rg "ErrorGroupState|errorGroup\.taskIdentifier" apps/webapp -A 3 -B 1

Repository: triggerdotdev/trigger.dev

Length of output: 12400


🏁 Script executed:

# Check for database schema or type definitions related to ErrorGroupState
fd "ErrorGroupState\|errorGroup" apps/webapp --extension ts --extension tsx | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Read the route file using a different method
file_path="apps/webapp/app/routes/_app.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.errors.\$fingerprint/route.tsx"
if [ -f "$file_path" ]; then
  sed -n '75,105p' "$file_path"
else
  echo "File not found at: $file_path"
  # Try listing the actual files
  find apps/webapp/app/routes -name "route.tsx" -path "*errors*fingerprint*" -type f
fi

Repository: triggerdotdev/trigger.dev

Length of output: 950


🏁 Script executed:

# Look at the Prisma schema for errorGroupState
rg "errorGroupState" --type ts --type prisma apps/ -B 2 -A 5 | grep -E "(model errorGroupState|taskIdentifier|field|@db)" | head -30

Repository: triggerdotdev/trigger.dev

Length of output: 99


🏁 Script executed:

# Find and inspect the Prisma schema file
fd "schema.prisma" apps/ internal-packages/

Repository: triggerdotdev/trigger.dev

Length of output: 116


🏁 Script executed:

# Search for errorGroupState model in Prisma schema
rg "model errorGroupState" internal-packages/database/prisma/schema.prisma -A 20

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Also search for any validation or constraints on taskIdentifier
rg "taskIdentifier" internal-packages/database/prisma/schema.prisma -B 3 -A 3

Repository: triggerdotdev/trigger.dev

Length of output: 2515


🏁 Script executed:

# Read the action handler to see how the data is persisted
sed -n '100,200p' "apps/webapp/app/routes/_app.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.errors.\$fingerprint/route.tsx" | head -80

Repository: triggerdotdev/trigger.dev

Length of output: 2799


🏁 Script executed:

# Look for where errorGroupState is updated
rg "errorGroupState.*update|errorGroupState.*create" apps/webapp --type ts --type tsx -A 5 | head -60

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Check the full Prisma schema for errorGroupState to see all fields
cat internal-packages/database/prisma/schema.prisma | grep -A 30 "model errorGroupState"

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Search for ErrorGroupActions class
rg "class ErrorGroupActions|export.*ErrorGroupActions" apps/webapp --type ts --type tsx -A 2 | head -40

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Find the file with ErrorGroupActions
fd "ErrorGroupActions" apps/webapp --type ts --type tsx

Repository: triggerdotdev/trigger.dev

Length of output: 238


🏁 Script executed:

# Try to get the errorGroupState model from Prisma schema
sed -n '/^model errorGroupState/,/^}/p' internal-packages/database/prisma/schema.prisma

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Find ErrorGroupActions
rg "ErrorGroupActions" apps/webapp -l

Repository: triggerdotdev/trigger.dev

Length of output: 242


🏁 Script executed:

# Read the entire errorGroupState model definition
cat internal-packages/database/prisma/schema.prisma | grep -A 25 "^model errorGroupState"

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Search for where errorGroupState is created/updated in the webapp
rg "errorGroupState\.(create|update)" apps/webapp -B 2 -A 5

Repository: triggerdotdev/trigger.dev

Length of output: 832


🏁 Script executed:

# Read the ErrorGroupActions service
wc -l apps/webapp/app/v3/services/errorGroupActions.server.ts

Repository: triggerdotdev/trigger.dev

Length of output: 128


🏁 Script executed:

# Read key methods from ErrorGroupActions
cat apps/webapp/app/v3/services/errorGroupActions.server.ts | head -300

Repository: triggerdotdev/trigger.dev

Length of output: 4328


Validate non-empty identifiers and positive ignore thresholds server-side.

z.string() accepts empty strings, and z.coerce.number() accepts zero or negative values. These can be persisted directly to the database without constraints, allowing crafted POSTs to create errorGroupState entries with empty taskIdentifier or invalid ignore conditions despite client-side min={1} attributes.

🔧 Tighten the action schema
 const actionSchema = z.discriminatedUnion("action", [
   z.object({
     action: z.literal("resolve"),
-    taskIdentifier: z.string(),
+    taskIdentifier: z.string().trim().min(1),
     resolvedInVersion: z.string().optional(),
   }),
   z.object({
     action: z.literal("ignore"),
-    taskIdentifier: z.string(),
-    duration: z.coerce.number().optional(),
-    occurrenceRate: z.coerce.number().optional(),
-    totalOccurrences: z.coerce.number().optional(),
+    taskIdentifier: z.string().trim().min(1),
+    duration: z.coerce.number().positive().optional(),
+    occurrenceRate: z.coerce.number().positive().optional(),
+    totalOccurrences: z.coerce.number().positive().optional(),
     reason: z.string().optional(),
   }),
   z.object({
     action: z.literal("unresolve"),
-    taskIdentifier: z.string(),
+    taskIdentifier: z.string().trim().min(1),
   }),
 ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const actionSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("resolve"),
taskIdentifier: z.string(),
resolvedInVersion: z.string().optional(),
}),
z.object({
action: z.literal("ignore"),
taskIdentifier: z.string(),
duration: z.coerce.number().optional(),
occurrenceRate: z.coerce.number().optional(),
totalOccurrences: z.coerce.number().optional(),
reason: z.string().optional(),
}),
z.object({
action: z.literal("unresolve"),
taskIdentifier: z.string(),
}),
]);
const actionSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("resolve"),
taskIdentifier: z.string().trim().min(1),
resolvedInVersion: z.string().optional(),
}),
z.object({
action: z.literal("ignore"),
taskIdentifier: z.string().trim().min(1),
duration: z.coerce.number().positive().optional(),
occurrenceRate: z.coerce.number().positive().optional(),
totalOccurrences: z.coerce.number().positive().optional(),
reason: z.string().optional(),
}),
z.object({
action: z.literal("unresolve"),
taskIdentifier: z.string().trim().min(1),
}),
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 80 - 98, The actionSchema currently allows empty taskIdentifier and
non-positive/zero ignore thresholds; update the discriminated union to enforce
taskIdentifier as a non-empty string (e.g., min length 1) and make duration,
occurrenceRate, and totalOccurrences coerce to numbers and require them to be
positive (e.g., .min(1) or .positive()) so empty/zero/negative values are
rejected server-side; apply these validators to the relevant branches in
actionSchema (the "resolve", "ignore", and "unresolve" objects) so
taskIdentifier, duration, occurrenceRate, and totalOccurrences are validated
before persisting.

Comment on lines +81 to +150
const { emails, webhooks, slackChannel, slackIntegrationId } = submission.value;

const existingChannels = await prisma.projectAlertChannel.findMany({
where: {
projectId: project.id,
alertTypes: { has: "ERROR_GROUP" },
environmentTypes: { has: environment.type },
},
});

const service = new CreateAlertChannelService();
const environmentTypes = [environment.type];
const processedChannelIds = new Set<string>();

for (const email of emails) {
const options: CreateAlertChannelOptions = {
name: `Error alert to ${email}`,
alertTypes: ["ERROR_GROUP"],
environmentTypes,
deduplicationKey: `error-email:${email}:${environment.type}`,
channel: { type: "EMAIL", email },
};
const channel = await service.call(project.externalRef, userId, options);
processedChannelIds.add(channel.id);
}

if (slackChannel) {
const [channelId, channelName] = slackChannel.split("/");
if (channelId && channelName) {
const options: CreateAlertChannelOptions = {
name: `Error alert to #${channelName}`,
alertTypes: ["ERROR_GROUP"],
environmentTypes,
deduplicationKey: `error-slack:${environment.type}`,
channel: {
type: "SLACK",
channelId,
channelName,
integrationId: slackIntegrationId,
},
};
const channel = await service.call(project.externalRef, userId, options);
processedChannelIds.add(channel.id);
}
}

for (const url of webhooks) {
const options: CreateAlertChannelOptions = {
name: `Error alert to ${new URL(url).hostname}`,
alertTypes: ["ERROR_GROUP"],
environmentTypes,
deduplicationKey: `error-webhook:${url}:${environment.type}`,
channel: { type: "WEBHOOK", url },
};
const channel = await service.call(project.externalRef, userId, options);
processedChannelIds.add(channel.id);
}

const channelsToDelete = existingChannels.filter(
(ch) =>
!processedChannelIds.has(ch.id) &&
ch.alertTypes.length === 1 &&
ch.alertTypes[0] === "ERROR_GROUP"
);

for (const ch of channelsToDelete) {
await prisma.projectAlertChannel.delete({ where: { id: ch.id } });
}

return json({ ok: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The form saves channels, but not the error alert configuration.

This action only forwards transport details into CreateAlertChannelService. Its create/update paths in apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts never write errorAlertConfig, so saving from this panel will leave those channels with a null config and the evaluator has nothing to parse. Please persist the validated alert settings before replacing existing channels.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
around lines 81 - 150, The action creates/replaces channels via
CreateAlertChannelService.call using CreateAlertChannelOptions but never
persists the alert configuration (errorAlertConfig), so channels end up with
null configs; update the route to include the validated errorAlertConfig in the
options passed to CreateAlertChannelService.call (for email, slack and webhook
flows) and ensure CreateAlertChannelService
(apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts) writes that
errorAlertConfig to the DB on both create and update paths; reference the
CreateAlertChannelOptions type, CreateAlertChannelService.call invocations, and
the errorAlertConfig field when adding the config to the options before calling
the service and before deleting/replacing existing channels.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants